23.05.18
오늘 한 일
- 알고리즘 문제 풀이
- 포트폴리오 정리
- 익스텐션 개발
- 블로그 정리
-
프로그라피 플젝 시작?? 내일 디자인 나오면 바로 회의 -
zustand
공식문서 읽기
알고리즘 문제 풀이
오늘 알고리즘 문제 두개를 풀었다.
2021 Dev-Matching: 웹 백엔드 개발자(상반기)
문제 세 개중에 두 개를 풀었다.
문제를 풀기 전에 꼼꼼히 읽고 설계해서 구현하는 연습을 했다.
1번 문제(Level 1) 문제 링크
주석을 달아서 설계를 먼저 하고 구현했다.
/**
1. 0의 개수 확인
2. 0이 아닌 로또 번호가 일치한 것들을 확인하여 일치 개수 세기
3. 최저 순위는 일치 개수, 최고 순위는 일치 개수 + 0 개수
*/
function solution(lottos, win_nums) {
const winCountToRank = {
6: 1,
5: 2,
4: 3,
3: 4,
2: 5,
1: 6,
0: 6,
};
let zero = 0;
let winCount = 0;
lottos.forEach(lotto => {
if (lotto === 0) return (zero += 1);
if (win_nums.some(win_num => win_num === lotto)) winCount += 1;
});
return [winCountToRank[winCount + zero], winCountToRank[winCount]];
}
2번 문제(Level 2) 문제 링크
/**
(x1,y1), (x2,y2)
예를 들어 (2,2) (5,4) 라고 하면
2,2 2,3 2,4
3,2 3,4
4,2 4,4
5,2 5,3 5,4
x1은 y1~y2 까지
x2도 y1~y2 까지
나머지는 (x1+a, y1), (x2+a,y2)
현재 이동하는 방향을 체크해서, 움직일 수 있으면 그 자리에 있던 값을 따로 저장 후 자리로 이동한다.
처음 세팅을 초기 위치에서 오른쪽으로 이동한다.
우, 하, 좌, 상을 돌고 초기 위치가 되면 멈춘다.
오른쪽 - y 증가 / 아래쪽 - x 증가 / 왼쪽 - y 감소 / 위쪽 - x 감소
이동 시킨 값들을 배열에 저장해두고, 이동이 끝나면 최소값을 받아와 result 배열에 추가한다.
모든 쿼리가 끝나면 result를 반환한다.
*/
function solution(rows, columns, queries) {
//행렬 세팅
const arr = [];
let num = 1;
for (let row = 0; row <= rows; row++) {
arr.push([]);
for (let col = 0; col <= columns; col++) {
if (row === 0 || col === 0) {
arr[row][col] = 0;
continue;
}
arr[row][col] = num++;
}
}
const result = [];
queries.forEach(query => {
const [x1, y1, x2, y2] = query;
let directionArr = [
[0, 1],
[1, 0],
[0, -1],
[-1, 0],
]; // 우 하 좌 상
let dIndex = 0;
const rotateArr = [];
let [x, y] = [x1, y1];
let temp = arr[x][y];
let min = arr[x][y];
while (true) {
const [dx, dy] = directionArr[dIndex];
const [newX, newY] = [x + dx, y + dy];
// 범위 벗어났을 때 방향 바꾸기
if (newX < x1 || newX > x2 || newY < y1 || newY > y2) {
if (dIndex === 3) break;
dIndex += 1;
continue;
}
let cur = temp;
let nxt = arr[newX][newY];
arr[newX][newY] = cur;
temp = nxt;
if (min > temp) min = temp;
x = newX;
y = newY;
}
result.push(min);
});
return result;
}
확실히 꼼꼼히 읽고 설계한 다음에 푸니 잘 되는 느낌이다. 앞으로도 이런 방법으로 계속 풀어야겠다.
익스텐션 개발
어제 해결못한 에러를 오늘도 이어서 해결했다.
과제 리스트에 동영상 과제와 일반 과제를 받는데, 이걸 하나의 배열에 담아야했다. 그래서 useState
를 통해 activityList
에 담았다. 근데 setState할 때 flat 처리를 하지 않아서 에러가 났었다. 그래서 flat 처리를 해주었다.
const activities = await allProgress(
courses.map(course => getActivities(course.id)),
progress => setPos(progress)
).then(activities => activities.flat());
그리고 tailwindCSS가 잘 안 먹어서 제공하는 클래스 전부 important 처리를 해주도록 설정했다.
// tailwind.config.js
module.exports = {
important: true,
// ...
};
그러고 1.0.6 버전을 배포했는데, 버튼 hover할 때 애니메이션이 작동하지 않는 버그가 있었다. 위에서 important 처리한 것 때문에 그런 것 같다. 그래서 애니메이션 작동하도록 설정을 추가했다.
initial과 whileHover에서 width, height를 설정해주었다.
<motion.div
initial={{ width: '40px', height: '40px' }}
whileHover={{ width: '100px', height: '50px' }}
className=" rounded-[50px] bg-[#2F6EA2] shadow-md shadow-[#2F6EA2] cursor-pointer"
onClick={() => setIsModalOpen(prev => !prev)}
></motion.div>
E2E 테스트를 위해 Playwright 세팅
세팅을 하다가 한 에러가 떴다.
공식문서에 나와있는대로 fixtures.ts
파일을 만들어서 커스텀한 test
함수를 만들고 다른 테스트 코드들에서 import해서 사용하려고 했다.
// fixtures.ts
import { test as base, expect, chromium, type BrowserContext } from '@playwright/test';
import path from 'path';
export const test = base.extend<{
context: BrowserContext;
extensionId: string;
}>({
context: async ({}, use) => {
const pathToExtension = path.join(path.resolve(), './dist');
const context = await chromium.launchPersistentContext('', {
headless: false,
args: [
`--headless=new`,
`--disable-extensions-except=${pathToExtension}`,
`--load-extension=${pathToExtension}`,
],
});
await use(context);
await context.close();
},
extensionId: async ({ context }, use) => {
let [background] = context.serviceWorkers();
if (!background) background = await context.waitForEvent('serviceworker');
const extensionId = background.url().split('/')[2];
await use(extensionId);
},
});
export { expect };
이러고 터미널에 yarn playwright test
를 입력하니까 Cannot find module
에러가 떴다. fixtures.ts 파일을 인식하지 못한다.
이게 왜 그런가 찾아보니 내 package.json
에 type
이 module
로 되어있어서 그런 것이었다. 이 부분을 지우면 테스트는 잘 돌아갔다.
"type": "module"
을 추가하면 ES6 Module을 사용할 수 있다.
만약 type을 설정하지 않았다면, node.js가 CommonJS를 사용한다고 가정한다.
타입스크립트에서module
을ESNext
로 설정하면 ES6 Module을 사용할 수 있고,moduleResolution
을node
로 설정하면 Node.js의 모듈 시스템을 사용할 수 있다.
하지만, package.json
에 저 부분을 지우면 CRXJS를 사용할 수 없다. 그래서 저 부분을 유지하면서 해결할 방법을 찾아보았다.
tsconfig.json
에moduleResolution
을node
로 설정하기 근데 이건 이미 설정되어있어서 안 된다.@crxjs/vite-plugin
을 사용할 때@crxjs/vite-plugin/dist/index.mjs
로 해두고package.json
에 type 지우기 github issue에서 찾은 방법이다. 근데 이 방법도 안 된다.import { test, expect } from './fixtures.js'
형태로 임포트하기 issue 이 방법은 된다. 그래서 이 방법으로 해결했다.
왜 이런 문제가 생길까?
임포트할 때 파일 확장자가 없이 가져올 수 있는 것은 CJS일 때만 가능하다. 근데 package.json에서 type을 module로 세팅했으므로 ES6 Module을 사용하게 되고, ES6 Module은 파일 확장자를 붙여야만 가져올 수 있다. 그래서 모듈을 인식하지 못한 문제가 발생한 듯 하다. 파일 확장자를 붙이면 CJS와 ESM 둘다 사용이 가능하다.타입스크립트 공식문서에서도 잘 설명해주고 있다.
이를 해결하기 위해 파일 확장자에 mts
나 cts
이렇게 명시하면 각각 ESM, CJS 형태로 인식한다는데 솔직히 이건 너무 이상하다.
난 저런 확장자를 쓰기 싫어서 그냥 import { test, expect } from './fixtures.js'
이렇게 써서 해결했다.
Sentry - 에러 로그 수집하기
내 계정으로 테스트하는 것만으로는 에러들을 확인하기 힘들었다. 사용자들이 겪은 에러들을 수집하기 위해 Sentry
를 사용하기로 했다.
사용해보니까 되게 자세하게 에러를 수집해준다. 사용자가 어떤 행동을 취했을 때 에러가 발생했는지 추적할 수 있었다. new Sentry.Replay()
를 integrations에 추가하면 에러 발생 시점의 사용자의 행동을 재현해주어 동영상으로 확인까지 할 수 있다!! 되게 신기했다.
// src/pages/content/main.tsx
import * as Sentry from '@sentry/react';
Sentry.init({
dsn: process.env.SENTRY_DSN,
release: version,
integrations: [
new Sentry.Integrations.Breadcrumbs({ console: true }),
new Sentry.BrowserTracing(),
],
tracesSampleRate: 1.0,
replaysSessionSampleRate: 0.1,
replaysOnErrorSampleRate: 1.0,
});
하지만 걱정되는게 하나 있었다. 이렇게 로그를 수집하는게 사용자의 개인정보를 수집하는 것은 아닐까? 약간 조심스러워져서 배포 전에 한 번 더 확인해봐야겠다.
Sentry에서 ErrorBoundary도 제공해주고 있다. 에러가 throw되면 그대로 익스텐션이 터져버리는게 아니라, ErrorBoundary가 이를 확인하고 에러를 잡아주면서 fallback 컴포넌트를 보여준다.
import { ErrorBoundary } from '@sentry/react';
const App = () => {
return (
<ErrorBoundary fallback={<Fallback />}>
<Main />
</ErrorBoundary>
);
};
try/catch 문과 같은 느낌이다. 근데 try/catch 문을 사용하면 코드가 지저분해진다.
리액트에서는 선언형 프로그래밍이 특징이라고 많이 들었다. 이게 처음엔 와닿지 않았는데, 저번에 ErrorBoundary에 대한 내용을 작성한 글을 보고 이해할 수 있었다.
선언형과 명령형이 있는데, 명령형은 어떻게 보여줄지에 집중하고, 선언형은 무엇을 보여줄지에 집중한다.
이게 무슨 소리냐하면 명령형은 if/else, for, try/catch 등을 사용해서 어떻게 보여줄지를 직접 작성해주는 것이고, 선언형은 이런 것들을 사용하지 않고 무엇을 보여줄지를 작성해주는 것이다.
선언형 프로그래밍을 통해 어떻게 보여줄지에 대해서는 이미 메서드가 해주고 있기 때문에 개발자는 무엇을 보여줄지에 대해서만 집중하면 된다. 그래서 코드가 깔끔해진다. 추상화를 하여 신경써야할 부분을 줄여준다.
ErrorBoundary
도 마찬가지다. 에러가 발생하면 어떻게 처리할지를 직접 작성해주는 것이 아니라, 에러가 발생하면 어떤 컴포넌트를 보여줄지를 작성해주는 것이다. 그래서 코드가 깔끔해진다.
Suspense
를 사용할 때도 마찬가지다. Suspense
는 어떤 컴포넌트를 보여줄지를 작성해주는 것이다.
이렇게 선언적으로 작성하면 가독성 좋게 코드를 작성할 수 있다.
다음은 이 글에서 발췌한 코드이다.
// 명령형으로 짠 코드
import { useState } from 'react';
import { useQuery } from 'react-query';
import UserInfo from './components/UserInfo';
import UserInfoSkeleton from './components/UserInfo/index.skeleton';
import Alarm from './components/Alarm';
import AlarmSkeleton from './components/Alarm/index.skeleton';
import Retry from '../shared/Retry';
import { getUserInfo, getAlarm } from '../fetches';
const User = () => {
const {
data: userInfoData,
isLoading: userInfoIsLoading,
error: userInfoError,
refetch: userInfoRefetch
} = useQuery(['userInfo'], getUserInfo);
const {
data: alarmData,
isLoading: alarmIsLoading,
error: alarmError,
refetch: alarmRefetch
} = useQuery(['alarm'], getAlarm);
return (
<section className="user__container">
{
userInfoIsLoading && alarmIsLoading && (
<>
<UserInfoSkeleton/>
<AlarmSkeleton/>
</>
) : (
<>
{
userInfoError ? (
<Retry handleRetry={refetchUserInfo}/>
) : (
<UserInfo data={userInfoData!}/>
)
}
{
alarmError ? (
<Retry handleRetry={refetchAlarm}/>
) : (
<Alarm data={alarmData!}/>
)
}
</>
)
}
</section>
)
}
export default User;
// 선언형으로 짠 코드
import { Suspense } from 'react';
import Banner from '../Banner';
import BannerSkeleton from '../Banner/index.skeleton';
import CustomMenu from '../CustomMenu';
import CustomMenuSkeleton from '../CustomMenu/index.skeleton';
import User from '../User/index';
const User = () => (
<section className="user__container">
<RetryErrorBoundary>
<Suspense fallback={<UserInfoSkeleton />}>
<UserInfo />
</Suspense>
</RetryErrorBoundary>
<RetryErrorBoundary>
<Suspense fallback={<AlarmSkeleton />}>
<Alarm />
</Suspense>
</RetryErrorBoundary>
</section>
);
export default User;
무엇을 보여줄지에 대해서만 집중할 수 있어서 굉장히 깔끔해진 것을 볼 수 있다.
이렇게 선언형 프로그래밍을 통해 추상화를 하면서 코드를 깔끔하게 작성할 수 있다는 것을 알게 되었다.
나도 Sentry에서 제공하는 ErrorBoundary를 사용해서 선언적으로 작성해야겠다. 그럴려면 일단 예외처리를 꼼꼼하게 해야겠다!
내일 할 일
- 알고리즘 문제 풀이 (구현문제)
- 포트폴리오 작성
- 2023 GDG Campus Korea 사람을 찾습니다 : 너 내 동료가 되라! 신청
- Nest.js 강의 듣기
- 프로그라피 플젝 시작??
- 익스텐션 개발
- Sentry 적용
- ErrorBoundary 적용
- E2E 테스트 코드 작성
- Jest로 Unit 테스트 코드 작성
- Sentry 적용